package topics

import (
	"context"
	"encoding/json"
	"io/ioutil"
	"os"
	"sort"
	"strings"
	"time"

	redis "github.com/go-redis/redis/v8"
	"go.mongodb.org/mongo-driver/bson/primitive"

	"github.com/go-kit/kit/log"
)

// NumberService维护一个Redis的计数器，每次新增或者修改关键字专题时计数器就会加1，表示最新的版本号
type NumberService interface {
	// 每次调用时默认为更新操作，更新版本号+1并返回
	GetNumber(ctx context.Context) (int64, error)
	// 返回当前版本号
	ReadNumber(ctx context.Context) (int64, error)
	// 初始化时用于设置版本号
	SetNumber(ctx context.Context, version int64) error
}

type smartTopicsService struct {
	Cache []SmartTopic
	Total int
}

// 智能专题相关服务
// 业务：目前智能专题是内置专题不可更改，初始化服务时获取智能专题配置并缓存到内存
type SmartTopicsService interface {
	// 获取缓存中的全量智能专题
	Get(ctx context.Context) []SmartTopic
	// 返回智能专题的个数
	Length(ctx context.Context) int
}

func NewSmartTopicsService() SmartTopicsService {
	var cache []SmartTopic
	smartTopicData, err := ioutil.ReadFile("zhuanti.json")
	if err != nil {
		panic(err)
	}
	err = json.Unmarshal(smartTopicData, &cache)
	if err != nil {
		panic(err)
	}
	return &smartTopicsService{
		Cache: cache,
		Total: len(cache),
	}
}

func (svc *smartTopicsService) Get(_ context.Context) []SmartTopic {
	return svc.Cache
}

func (svc *smartTopicsService) Length(_ context.Context) int {
	return svc.Total
}

type numberService struct {
	key         string
	redisClient *redis.Client
}

func NewNumberService(key string, redisClient *redis.Client) *numberService {
	return &numberService{
		key:         key,
		redisClient: redisClient,
	}
}

func (svc *numberService) GetNumber(ctx context.Context) (int64, error) {
	return svc.redisClient.Incr(ctx, svc.key).Result()
}

func (svc *numberService) ReadNumber(ctx context.Context) (int64, error) {
	return svc.redisClient.Get(ctx, svc.key).Int64()
}

func (svc *numberService) SetNumber(ctx context.Context, version int64) error {
	return svc.redisClient.Set(ctx, svc.key, version, 0).Err()
}

type simpleKeywordTopicService struct {
	repo Repository
}

func NewKeywordTopicTreeService(repo Repository) KeywordTopicTreeService {
	return &simpleKeywordTopicService{repo: repo}
}

type KeywordTopicTreeService interface {
	Get(ctx context.Context) ([]*SimpleKeywordTopic, error)
}

func (svc *simpleKeywordTopicService) Get(ctx context.Context) ([]*SimpleKeywordTopic, error) {
	topics, err := svc.repo.Search(ctx, SearchOpts{Enabled: true}, true)
	if err != nil {
		return nil, err
	}
	tree := make(map[string]*SimpleKeywordTopic)
	var topTopics []*SimpleKeywordTopic
	for _, topic := range topics {
		tree[topic.Id] = &SimpleKeywordTopic{Id: topic.Id, Name: topic.Name}
	}
	for _, topic := range topics {
		if topic.ParentId == nil {
			topTopics = append(topTopics, tree[topic.Id])
		} else {
			if parent, ok := tree[*topic.ParentId]; ok {
				parent.Children = append(parent.Children, tree[topic.Id])
				if tree[topic.Id].Parent != nil && tree[topic.Id].Parent.Parent == tree[topic.Id] {
					continue
				}
				tree[topic.Id].Parent = parent
			}
		}
	}
	return topTopics, nil
}

type topic struct {
	Id       string     `json:"id"`
	Name     string     `json:"name"`
	Keywords []Keywords `json:"keywords"`
	ParentId *string    `json:"parentId,omitempty"`
}

func Totopic(t *Topic) *topic {
	return &topic{
		Id:       t.Id,
		Name:     t.Name,
		Keywords: t.Keywords,
		ParentId: t.ParentId,
	}
}

type UpgradeService interface {
	// 更新事件的循环，每五秒钟进行一次判断查看是否需要进行更新
	Loop(ctx context.Context, done chan struct{}, sigs chan os.Signal)
}

type upgradeService struct {
	tr      Repository
	ns      NumberService
	repo    UpgradeRepository
	logger  log.Logger
	version int64
	key     string
}

func NewUpgradeService(ctx context.Context, r Repository, ns NumberService, repo UpgradeRepository, logger log.Logger, key string) *upgradeService {
	svc := &upgradeService{
		tr:     r,
		ns:     ns,
		repo:   repo,
		key:    key,
		logger: logger,
	}
	// 服务初始化时需要将数据库中的version同步到Topic_version
	// 并且读取latest_version同步NumberService的初始值
	// 之后更新按照latest_version与topic_version的大小进行判断
	var topicVersion int64
	// 若mysql中没有关键字专题，则初始化topic_version为0
	topicMysqlVersion, _ := svc.tr.GetVersion(ctx)
	if topicMysqlVersion != nil {
		topicVersion = *topicMysqlVersion
	} else {
		topicVersion = 0
	}
	err := svc.ns.SetNumber(ctx, topicVersion)
	if err != nil {
		_ = svc.logger.Log("redis set number error", err.Error())
	}
	// 读取latest_version，如果存在则同步成员变量为latest_version的值
	// 如果不存在则同步成员变量为0，则执行upgrade更新方法时同步为topic_version
	version, err := svc.repo.GetVersion(ctx, svc.key)
	if err == nil {
		svc.version = version
	} else {
		_ = svc.logger.Log("redis get latest_version error", err.Error())
		svc.version = 0
	}
	svc.upgrade(ctx)
	return svc
}

func (svc *upgradeService) Loop(ctx context.Context, done chan struct{}, sigs chan os.Signal) {
	ticker := time.NewTicker(30 * time.Second)
	defer func() {
		done <- struct{}{}
	}()
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			svc.upgrade(ctx)
		case <-sigs:
			return
		}
	}
}

func (svc *upgradeService) upgrade(ctx context.Context) {
	topicVersion, err := svc.ns.ReadNumber(ctx)
	if err != nil {
		_ = svc.logger.Log("numberService error", err.Error())
		return
	}
	if svc.version >= topicVersion {
		_ = svc.logger.Log("version is latest", "doesn`t need to update")
		return
	}
	// 筛选出所有启用的配置且若有父专题同样启用
	topics, err := svc.tr.Search(ctx, SearchOpts{Enabled: true}, true)
	if err != nil {
		_ = svc.logger.Log("topics search error:", err.Error())
		return
	}
	var ts []*topic
	temp := make(map[string]*Topic)
	for _, topic := range topics {
		if topic.ParentId == nil {
			temp[topic.Id] = topic
		}
	}
	for _, topic := range topics {
		if topic.ParentId != nil {
			if parent, ok := temp[*topic.ParentId]; ok {
				parent.Children = append(parent.Children, *topic)
			}
		}
	}
	for _, t := range temp {
		if len(t.Keywords) == 0 {
			continue
		}
		for _, children := range t.Children {
			if len(t.Keywords) == 0 {
				continue
			}
			ts = append(ts, Totopic(&children))
		}
		ts = append(ts, Totopic(t))
	}
	json_data, err := json.Marshal(ts)
	if err != nil {
		_ = svc.logger.Log("json marshal error", err.Error())
		return
	}
	err = svc.repo.Upgrade(ctx, svc.key, string(json_data), topicVersion)
	if err != nil {
		_ = svc.logger.Log("redis update error", err.Error())
		return
	}
	svc.version = topicVersion
	_ = svc.logger.Log("updated", "redis topic update success")
	return
}

type service struct {
	repo          Repository
	numberService NumberService
	logger        log.Logger
}

// 关键字专题相关的CRUD操作
type Service interface {
	CreateTopic(ctx context.Context, topic *Topic) (*Topic, error)
	UpdateTopic(ctx context.Context, topic *Topic) (*Topic, error)
	DeleteTopic(ctx context.Context, id string) error
	GetTopic(ctx context.Context, id string) (*Topic, error)
	GetTopics(ctx context.Context, page, size int) ([]Topic, int, error)
}

func NewService(r Repository, numberService NumberService, logger log.Logger) *service {
	svc := &service{
		repo:          r,
		numberService: numberService,
		logger:        logger,
	}
	// Service依赖于NumberService用于创建或修改topic时获取最新的版本号
	// 因此初始化时需要将NumberService的初始值设置为mysql中最新的版本号
	ctx := context.Background()
	topicVersion, err := svc.repo.GetVersion(ctx)
	if err != nil {
		_ = svc.logger.Log("mysql get version error", err.Error())
	}
	if topicVersion != nil {
		err = svc.numberService.SetNumber(ctx, *topicVersion)
		if err != nil {
			_ = svc.logger.Log("numberService set number error", err.Error())
		}
	}
	return svc
}

func (s *service) normalizeTopic(topic *Topic) *Topic {
	var keywords []Keywords
	for _, k := range topic.Keywords {
		if k.Op != "include" && k.Op != "exclude" {
			continue
		}
		keys := strings.Fields(k.Keywords)
		if len(keys) == 0 {
			continue
		}
		keywords = append(keywords, Keywords{Op: k.Op, Keywords: strings.Join(keys, " ")})
	}
	topic.Keywords = keywords
	return topic
}

func (s *service) GetTopics(ctx context.Context, page, size int) ([]Topic, int, error) {
	topics, err := s.repo.Search(ctx, SearchOpts{})
	if err != nil {
		return nil, 0, err
	}
	// 使用字典将全量关键字专题列表的父子关系进行组织
	temp := make(map[string]*Topic)
	for _, topic := range topics {
		if topic.ParentId == nil {
			temp[topic.Id] = topic
		}
	}
	for _, topic := range topics {
		if topic.ParentId != nil {
			if father, ok := temp[*topic.ParentId]; ok {
				father.Children = append(father.Children, *topic)
			}
		}
	}
	var ts []Topic
	for _, topic := range temp {
		ts = append(ts, *topic)
	}
	for _, topic := range ts {
		sort.SliceStable(topic.Children, func(i, j int) bool {
			if topic.Children[i].CreateTimestamp < topic.Children[j].CreateTimestamp {
				return true
			} else if topic.Children[i].CreateTimestamp == topic.Children[j].CreateTimestamp {
				return topic.Children[i].Name < topic.Children[j].Name
			} else {
				return false
			}
		})
	}
	sort.SliceStable(ts, func(i, j int) bool {
		if ts[i].CreateTimestamp < ts[j].CreateTimestamp {
			return true
		} else if ts[i].CreateTimestamp == ts[j].CreateTimestamp {
			return ts[i].Name < ts[j].Name
		} else {
			return false
		}
	})
	// 判断分页范围，超出数量限制响应空数据
	if (page-1)*size >= len(ts) {
		return nil, 0, nil
	}
	var res []Topic
	// 正常数量范围时，返回请求页码数量范围的关键字专题
	if page*size >= len(ts) {
		res = ts[(page-1)*size:]
	} else {
		res = ts[(page-1)*size : page*size]
	}
	return res, len(ts), nil
}

func (s *service) CreateTopic(ctx context.Context, topic *Topic) (*Topic, error) {
	version, err := s.numberService.GetNumber(ctx)
	if err != nil {
		return nil, err
	}
	topic = s.normalizeTopic(topic)
	topic.Id = primitive.NewObjectID().Hex()
	topic.Version = version
	now := time.Now().UnixNano() / 1e6
	topic.CreateTimestamp = now
	topic.UpdateTimestamp = now
	err = s.repo.Save(ctx, topic)
	if err != nil {
		return nil, err
	}
	return topic, nil
}

func (s *service) UpdateTopic(ctx context.Context, topic *Topic) (*Topic, error) {
	number, err := s.numberService.GetNumber(ctx)
	if err != nil {
		return nil, err
	}
	s.normalizeTopic(topic)
	topic.Version = number
	topic.UpdateTimestamp = time.Now().UnixNano() / 1e6
	err = s.repo.Save(ctx, topic)
	if err != nil {
		return nil, err
	}
	return topic, nil
}

func (s *service) DeleteTopic(ctx context.Context, id string) error {
	topic, err := s.repo.Get(ctx, id)
	if err != nil {
		return err
	}
	err = s.repo.Delete(ctx, topic.Id)
	return err
}

func (s *service) GetTopic(ctx context.Context, id string) (*Topic, error) {
	topic, err := s.repo.Get(ctx, id)
	if err != nil {
		return nil, err
	}
	return topic, nil
}
